package it.crosato.stage.server.model.retriever;

import it.crosato.stage.shared.exceptions.DatabaseConnectionException;
import it.crosato.stage.shared.exceptions.NonExistentException;
import it.crosato.stage.shared.exceptions.ParsingException;
import it.crosato.stage.shared.exceptions.RetrievingException;
import it.crosato.stage.shared.objects.Reaction;

import java.io.IOException;
import java.io.StringReader;
import java.rmi.RemoteException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Vector;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.rpc.ServiceException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import keggapi.KEGGLocator;
import keggapi.KEGGPortType;

import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class ReactionRetriever implements IReactionRetriever {

	private IKGMLRetriever KGMLRetriever = new KGMLRetriever();

	/**
	 * Recupera una reazione gestendo internamente le interazioni con la banca
	 * dati interna e KEGG.
	 * @param id codice della reazione
	 * @param pathway codice della specifica via metabolica
	 * @param pathwayEnzymes lista degli enzimi presenti nella via metabolica
	 * @param time tempo di scadenza nella banca dati interna
	 * @return la reazione richiesta con tutte le sue informazioni
	 * @throws RetrievingException se si verificano problemi nel recupero
	 * @throws NonExistentException se la reazione o la via metabolica non esistono
	 */
	@Override
	public Reaction getReaction(String id, String pathway, Vector<String> pathwayEnzymes, long time) 
			throws RetrievingException, NonExistentException {

		Logger.getLogger("myLog").info("recupero reazione " + id);
		try {
			Reaction reaction = searchInDB(id, time);
			if (reaction == null) {
				reaction = searchInKEGG(id);
				if (reaction == null) {
					throw new NonExistentException(NonExistentException.REACTION);
				}
				storeInDB(reaction);
			}
			String kgml = KGMLRetriever.getKGML(pathway, time);
			getOtherInfo(reaction, pathway, pathwayEnzymes, kgml);
			return reaction;
		}
		catch(DatabaseConnectionException dbce) {
			Logger.getLogger("myLog").info("dbce ---> " + dbce.getMessage());
			throw new RetrievingException(RetrievingException.REACTION);
		}
		catch(SQLException sqle) {
			Logger.getLogger("myLog").info("sqle ---> " + sqle.getMessage());
			throw new RetrievingException(RetrievingException.REACTION);
		}
		catch (RemoteException re) {
			Logger.getLogger("myLog").info("re ---> " + re.getMessage());
			throw new RetrievingException(RetrievingException.REACTION);
		}
		catch (ServiceException se) {
			Logger.getLogger("myLog").info("se ---> " + se.getMessage());
			throw new RetrievingException(RetrievingException.REACTION);
		}
		catch (ParsingException pe) {
			Logger.getLogger("myLog").info("pe ---> " + pe.getMessage());
			throw new RetrievingException(RetrievingException.REACTION);
		}
		finally {
			DatabaseConnection.closeConnection();
		}
	}

	/**
	 * Recupera tutte le reazioni di una via metabolica gestendo internamente le
	 * interazioni con la banca dati interna e KEGG.
	 * @param pathway codice della via metabolica
	 * @param reactionsIds lista dei codici delle reazioni della via metabolica
	 * @param pathwayEnzymes lista degli enzimi presenti nella via metabolica
	 * @param time tempo di scadenza nella banca dati interna
	 * @return la lista delle reazioni, con tutte le loro informazioni, presenti nella
	 * via metabolica
	 * @throws RetrievingException se si verificano problemi nel recupero
	 * @throws NonExistentException se la via metabolica non esiste
	 */
	@Override
	public Vector<Reaction> getPathwayReactions(String pathway, Vector<String> reactionsIds,
			Vector<String> pathwayEnzymes, long time) throws RetrievingException, NonExistentException {

		try {
			String kgml = KGMLRetriever.getKGML(pathway, time);
			Vector<Reaction> reactions = new Vector<Reaction>();
			for (int i = 0; i < reactionsIds.size(); i++) {
				Logger.getLogger("myLog").info("recupero reazione " + reactionsIds.get(i));
				Reaction reaction = searchInDB(reactionsIds.get(i), time);
				if (reaction == null) {
					reaction = searchInKEGG(reactionsIds.get(i));
					if (reaction == null) {
						throw new NonExistentException(NonExistentException.REACTION);
					}
					storeInDB(reaction);
				}
				getOtherInfo(reaction, pathway, pathwayEnzymes, kgml);
				reactions.add(reaction);
			}
			return reactions;
		}
		catch(DatabaseConnectionException dbce) {
			Logger.getLogger("myLog").info("dbce ---> " + dbce.getMessage());
			throw new RetrievingException(RetrievingException.REACTION);
		}
		catch(SQLException sqle) {
			Logger.getLogger("myLog").info("sqle ---> " + sqle.getMessage());
			throw new RetrievingException(RetrievingException.REACTION);
		}
		catch (RemoteException re) {
			Logger.getLogger("myLog").info("re ---> " + re.getMessage());
			throw new RetrievingException(RetrievingException.REACTION);
		}
		catch (ServiceException se) {
			Logger.getLogger("myLog").info("se ---> " + se.getMessage());
			throw new RetrievingException(RetrievingException.REACTION);
		}
		catch (ParsingException pe) {
			Logger.getLogger("myLog").info("pe ---> " + pe.getMessage());
			throw new RetrievingException(RetrievingException.REACTION);
		}
		finally {
			DatabaseConnection.closeConnection();
		}
	}

	/**
	 * Cerca nella banca dati interna le informazioni generali di una reazione chimica.
	 * @param id codice della reazione chimica
	 * @param time tempo di scadenza nella banca dati interna
	 * @return la reazione chimica con le sue informazioni generali oppure il valore nullo se
	 * la reazione non esiste nella banca dati interna o se è troppo vecchia
	 * @throws SQLException se si verificano dei problemi con le query
	 * @throws DatabaseConnectionException se si verificano problemi nella connessione
	 * alla banca dati interna
	 */
	private Reaction searchInDB(String id, long time)
	throws SQLException, DatabaseConnectionException {

		DatabaseConnection.openConnection();
		Statement st = DatabaseConnection.getStatement();
		ResultSet rs = st.executeQuery("SELECT count(*) FROM Reactions WHERE id='" + id + "';");
		rs.next();
		if (rs.getInt(1) != 0) {
			Logger.getLogger("myLog").info("reazione presente nel database");
			rs = st.executeQuery("SELECT * FROM Reactions WHERE id='" + id + "';");
			rs.next();
			Long date = rs.getLong("storeDate");
			if (date + time < new Date().getTime()) {
				Logger.getLogger("myLog").info("reazione troppo vecchia.");
				return null;
			}
			String equation = rs.getString("equation");
			String enzymesStr = rs.getString("enzymes");
			List<String> list = Arrays.asList(enzymesStr.split("\\s+"));
			Vector<String> enzymes = new Vector<String>(list);
			Logger.getLogger("myLog").info("recuperata reazione dal database");
			return new Reaction(id, equation, enzymes);
		}
		else {
			Logger.getLogger("myLog").info("reazione non presente nel database");
			return null;
		}
	}

	/**
	 * Cerca le informazioni generali di una reazione chimica in KEGG.
	 * @param id codice della reazione.
	 * @return le reazione con le sue informazioni generali oppure il valore nullo
	 * se tale reazione non esiste.
	 * @throws RemoteException se si verificano problemi nell'interazione remota con KEGG.
	 * @throws ServiceException se si verificano problemi legati ai servizi di KEGG.
	 */
	private Reaction searchInKEGG(String id) 
			throws RemoteException, ServiceException {

		Logger.getLogger("myLog").info("recupero reazione da KEGG");
		KEGGLocator locator = new KEGGLocator();
		KEGGPortType serv = locator.getKEGGPort();
		String reactionDescription = serv.bget(id);
		if (reactionDescription != null && ! reactionDescription.equals("")) {
			// equation
			Pattern pattern = Pattern.compile("EQUATION[\\s]*([^\\r\\n]*)");
			Matcher matcher = pattern.matcher(reactionDescription);
			matcher.find();
			String equation = matcher.group(1);
			// enzymes
			pattern = Pattern.compile("ENZYME[\\s]*([^\\r\\n]*)");
			matcher = pattern.matcher(reactionDescription);
			String enzymesStr = "";
			if (matcher.find()) {
				enzymesStr = matcher.group(1);
			}
			List<String> list = Arrays.asList(enzymesStr.split("\\s+"));
			Vector<String> enzymes = new Vector<String>(list);
			for (int i = 0; i < enzymes.size(); i++) {
				enzymes.set(i, "ec:" + enzymes.get(i));
			}
			Logger.getLogger("myLog").info("reazione recuperata da KEGG");
			return (new Reaction(id, equation, enzymes));
		}
		else {
			Logger.getLogger("myLog").info("reazione non presente in KEGG");
			return null;
		}
	}
	
	/**
	 * Recupera le informazioni specifiche di una reazione chimica all'interno di una data via
	 * metabolica.
	 * @param reaction la reazione chimica con le sue informazioni generali
	 * @param pathway codice della via metabolica
	 * @param pathwayEnzymes lista degli enzimi presenti nella via metabolica
	 * @param kgml file KGML relativo alla via metabolica
	 * @throws DatabaseConnectionException se si verificano problemi con la connessione alla banca
	 * dati interna.
	 * @throws SQLException se si verificano problemi con le query
	 * @throws ParsingException se si verificano problemi con il parsing del file KGML.
	 */
	private void getOtherInfo(Reaction reaction, String pathway, Vector<String> pathwayEnzymes,
			String kgml) throws DatabaseConnectionException, SQLException, ParsingException {
		DatabaseConnection.openConnection();
		Statement st = DatabaseConnection.getStatement();
		ResultSet rs = st.executeQuery("SELECT count(*) FROM PathwaysReactions WHERE reaction='" +
				reaction.getId() + "' AND pathway='" + pathway +"';");
		rs.next();
		if (rs.getInt(1) != 0) {
			rs = st.executeQuery("SELECT * FROM PathwaysReactions WHERE reaction='" +
					reaction.getId() + "' AND pathway='" + pathway +"';");
			rs.next();
			String enzymesStr = rs.getString("enzymes");
			List<String> list = Arrays.asList(enzymesStr.split(" "));
			Vector<String> enzymes = new Vector<String>(list);
			reaction.setEnzymes(enzymes);
			String type = rs.getString("tipo");
			if (type.equals("irreversible")) {
				reaction.setEquation(reaction.getEquation().replace("<=>", "=>"));
			}
		}
		else {
			String type = "reversible";
			if (! isReversibleKGML(kgml, reaction.getId())) {
				reaction.setEquation(reaction.getEquation().replace("<=>", "=>"));
				type = "irreversible";
			}
			setEnzymes(reaction, pathwayEnzymes);
			try {
				try {
					st.executeQuery("DELETE FROM PathwaysReactions WHERE reaction='" + reaction.getId() +
							"' AND pathway='" + pathway + "';");
				} catch(SQLException e) { }
				String enzymesStr = "";
				Vector<String> enzymes = reaction.getEnzymes();
				for (int i = 0; i < enzymes.size(); i++) {
					enzymesStr += enzymes.get(i);
					if (i < enzymes.size() - 1) {
						enzymesStr += " ";
					}
				}
				st.executeUpdate("INSERT INTO PathwaysReactions VALUES('" + reaction.getId() +
						"', '" + pathway + "', '" + enzymesStr + "', '" + type + "', '" + new Date().getTime() + "');");
				Logger.getLogger("myLog").info("inserita reazione nel database");
			}
			catch (SQLException sqle) {
				Logger.getLogger("myLog").info("sqle --> " + sqle.getMessage());
			}
		}
	}

	/**
	 * Recupera il tipo (reversibile o irreversibile) di una reazione dal file KGML.
	 * @param kgml il file KGML sotto forma di stringa.
	 * @param id codice della reazione chimica
	 * @return true se la reazione è reversibile, false altrimenti
	 * @throws ParsingException se si verificano problemi nel parsing del file.
	 */
	private boolean isReversibleKGML(String kgml, String id) throws ParsingException {
		try {
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			DocumentBuilder db = dbf.newDocumentBuilder();
			InputSource is = new InputSource(new StringReader(kgml));
			Document doc = db.parse(is);
			doc.getDocumentElement().normalize();
			XPathFactory factory = XPathFactory.newInstance();
			XPath xPath = factory.newXPath();
			XPathExpression xPathExp = xPath.compile("//reaction[contains(@name,'" + id + "')]/@type");
			String type = xPathExp.evaluate(doc, XPathConstants.STRING).toString();
			return type.equals("reversible");	
		}
		catch (ParserConfigurationException pce) {
			Logger.getLogger("myLog").info("pce ---> " + pce.getMessage());
			throw new ParsingException();
		}
		catch (SAXException saxe) {
			Logger.getLogger("myLog").info("saxe ---> " + saxe.getMessage());
			throw new ParsingException();
		}
		catch (IOException ioe) {
			Logger.getLogger("myLog").info("ioe ---> " + ioe.getMessage());
			throw new ParsingException();
		}
		catch (XPathExpressionException xpee) {
			Logger.getLogger("myLog").info("xpee ---> " + xpee.getMessage());
			throw new ParsingException();
		}
	}
	
	/**
	 * Seleziona tra gli enzimi della reazione generica solo quelli che sono presenti
	 * nella specifica via metabolica
	 * @param reaction la reazione con le sue informazioni generiche sugli enzimi
	 * @param pathwayEnzymes la lista degli enzimi presenti nella via metabolica
	 */
	private void setEnzymes(Reaction reaction, Vector<String> pathwayEnzymes) {
		for (int i = 0; i < reaction.getEnzymes().size(); i++) {
			if (! pathwayEnzymes.contains(reaction.getEnzymes().get(i))) {
				reaction.getEnzymes().remove(i);
				i--;
			}
		}
	}

	/**
	 * Memorizza nella banca dati interna le informazioni generiche relative ad una reazione,
	 * eventualmente sostituendo versioni precedenti. 
	 * @param reaction la reazione chimica da memorizzare con le sue informazioni.
	 * @throws DatabaseConnectionException se si verificano problemi con la connessione
	 * alla banca dati interna
	 */
	private void storeInDB(Reaction reaction) throws DatabaseConnectionException {
		Logger.getLogger("myLog").info("inserimento reazione nel database");
		DatabaseConnection.openConnection();
		Statement st = DatabaseConnection.getStatement();
		try {
			try {
				st.executeQuery("DELETE FROM Reactions WHERE id='" + reaction.getId() + "';");
			} catch(SQLException e) { }
			String enzymesStr = "";
			Vector<String> enzymes = reaction.getEnzymes();
			for (int i = 0; i < enzymes.size(); i++) {
				enzymesStr += enzymes.get(i);
				if (i < enzymes.size() - 1) {
					enzymesStr += " ";
				}
			}
			st.executeUpdate("INSERT INTO Reactions VALUES('" + reaction.getId() +
					"', '" + reaction.getEquation() + "', '" + enzymesStr + 
					"', '" + new Date().getTime() + "');");
			Logger.getLogger("myLog").info("inserita reazione nel database");
		}
		catch (SQLException sqle) {
			Logger.getLogger("myLog").info("sqle --> " + sqle.getMessage());
		}
	}
}